package com.tyros.common.utils;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 该类使用时需要依赖注入，修改为静态工具类方式
 * <p>
 * see RedisUtils {@link com.tyros.common.utils.RedisUtils}
 *
 * @author zhangshuaiyin
 * @date 2022/3/12 11:10
 */
@SuppressWarnings("unused")
public class RedisUtil {
    private static final Logger log = LoggerFactory.getLogger(RedisUtil.class);

    private static final String DISTRIBUTE_LOCK_KEY_FOR_CMD_LOG = "DISTRIBUTE_LOCK_KEY_FOR_CMD_LOG";
    private static final Integer RETRY_TIMES = 3;

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;

    public RedisUtil(RedisTemplate<String, Object> redisTemplate, StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
    }


    public void rename(String source, String target) {
        this.redisTemplate.rename(source, target);
    }

    public void delete(String key) {
        LogUtils.warn(log, "删除key: {}", key);
        this.stringRedisTemplate.delete(key);
    }

    public String getValue(String key) {
        return this.stringRedisTemplate.opsForValue().get(key);
    }

    public String getHashStringValue(String key, String field) {
        return (String) this.stringRedisTemplate.opsForHash().get(key, field);
    }

    public Map<Object, Object> getHashValue(String key) {
        return this.stringRedisTemplate.opsForHash().entries(key);
    }

    public void refreshExpireTime(String key, Long expireTime) {
        this.stringRedisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
    }

    public boolean put(String key, String value, Long expireTime) {
        this.stringRedisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);
        return true;
    }

    public boolean put(String key, String value) {
        this.stringRedisTemplate.opsForValue().set(key, value);
        return true;
    }

    public boolean put(String key, List<String> list) {
        String objString = JSONObject.toJSONString(list);
        this.stringRedisTemplate.opsForValue().set(key, objString);
        return true;
    }

    public List<String> getList(String key) {
        String str = this.stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(str)) {
            return Collections.emptyList();
        } else {
            return JSONObject.parseObject(str, new TypeReference<List<String>>() {
            });
        }
    }

    public boolean hasKey(String key) {
        Boolean hasKey = this.stringRedisTemplate.hasKey(key);
        return hasKey != null && hasKey;
    }

    public void cacheObject(String key, Object object, Long expireTime) {
        String objString = JSONObject.toJSONString(object);
        this.stringRedisTemplate.opsForValue().set(key, objString, expireTime, TimeUnit.MINUTES);
    }

    public void cacheObject(String key, Object object) {
        this.redisTemplate.opsForValue().set(key, object);
    }

    public void updateObject(String key, Object object) {
        Long expireTime = this.stringRedisTemplate.getExpire(key);
        String objString = JSONObject.toJSONString(object);
        if (expireTime != null) {
            this.stringRedisTemplate.opsForValue().set(key, objString, expireTime, TimeUnit.SECONDS);
        }
    }

    public Object getObject(String key, Class<?> clazz) {
        String objStr = this.stringRedisTemplate.opsForValue().get(key);
        return JSONObject.parseObject(objStr, clazz);
    }

    public void listLeftPush(String key, Object object) {
        String objString = JSONObject.toJSONString(object);
        this.stringRedisTemplate.opsForList().leftPush(key, objString);
    }

    public void listLeftPush(String key, Object object, Long expireTime) {
        String objString = JSONObject.toJSONString(object);
        this.stringRedisTemplate.opsForList().leftPush(key, objString);
        this.stringRedisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
    }

    public Object listRightPop(String key, Class<?> clazz) {
        String objStr = this.stringRedisTemplate.opsForList().rightPop(key);
        return JSONObject.parseObject(objStr, clazz);
    }

    public Object listGetLastElement(String key, Class<?> clazz) {
        String objStr = this.stringRedisTemplate.opsForList().index(key, -1L);
        return JSONObject.parseObject(objStr, clazz);
    }

    public Object listBrPop(final String key, Class<?> clazz) {
        List<Object> objects = this.stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
            connection.bRPop(1000, new byte[][]{key.getBytes()});
            return null;
        }, this.stringRedisTemplate.getValueSerializer());
        List<?> strList = GenericsUtils.castList(objects.get(0));
        String jsonString = (String) strList.get(1);
        return JSONObject.parseObject(jsonString, clazz);
    }

    public <T> List<T> listBatchPop(final String key, final Long count, Class<T> clazz) {
        final Long size = this.stringRedisTemplate.opsForList().size(key);
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                LogUtils.info(RedisUtil.log, "key:{} size: {}", key, size);
                if (size != null) {
                    if (count < size) {
                        operations.opsForList().range(key, 0L, count - 1L);
                    } else {
                        operations.opsForList().range(key, 0L, -1L);
                    }

                    operations.opsForList().trim(key, count, -1L);
                }

                return operations.exec();
            }
        };
        List<Object> result = GenericsUtils.castList(this.stringRedisTemplate.execute(sessionCallback), Object.class);
        if (!CollectionUtils.isEmpty(result)) {
            List<String> jsonStrList = GenericsUtils.castList(result.get(0), String.class);
            return JSONObject.parseArray(jsonStrList.toString(), clazz);
        } else {
            return Collections.emptyList();
        }
    }

    public <T> void listLeftPushAll(String key, List<T> list) {
        List<List<T>> partition = Lists.partition(list, 10000);
        partition.forEach((item) -> {
            String jsonString = JSONObject.toJSONString(list);
            List<String> targetList = JSONObject.parseArray(jsonString, String.class);
            this.stringRedisTemplate.opsForList().leftPushAll(key, targetList);
        });
    }

    public void listLeftBatchPush(String key, List<Object> objectList) {
        String objString = JSONObject.toJSONString(objectList);
        this.stringRedisTemplate.opsForList().leftPush(key, objString);
    }

    public List<Object> listBatchRightPop(String key, Class<Object> clazz) {
        String objStr = this.stringRedisTemplate.opsForList().rightPop(key);
        return JSONObject.parseArray(objStr, clazz);
    }

    public List<Object> listBatchBrPop(final String key, Class<Object> clazz) {
        List<Object> objects = this.stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
            connection.bRPop(1000, new byte[][]{key.getBytes()});
            return null;
        }, this.stringRedisTemplate.getValueSerializer());
        List<String> strList = GenericsUtils.castList(objects.get(0), String.class);
        String jsonString = strList.get(1);
        return JSONObject.parseArray(jsonString, clazz);
    }

    public void publish(String channel, String message) {
        LogUtils.info(log, "在{}发布{}", channel, message);
        this.stringRedisTemplate.convertAndSend(channel, message);
    }

    public Long setAdd(String key, Object value) {
        return this.redisTemplate.opsForSet().add(key, value);
    }

    public void setAdd(String key, Object... value) {
        this.redisTemplate.opsForSet().add(key, value);
    }

    public void setAdd(String key, Long expireTime, Object... value) {
        this.redisTemplate.opsForSet().add(key, value);
        this.redisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
    }

    public void setAdd(String key, Object value, Long expireTime) {
        this.redisTemplate.opsForSet().add(key, value);
        this.redisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
    }

    public Long setRemove(String key, Object object) {
        return this.redisTemplate.opsForSet().remove(key, object);
    }

    public Object setPop(String key) {
        return this.redisTemplate.opsForSet().pop(key);
    }

    public List<Object> setPop(String key, Long size) {
        return this.redisTemplate.opsForSet().pop(key, size);
    }

    public Long add(String key, Object value, Long expireTime) {
        Long res = this.redisTemplate.opsForSet().add(key, value);
        this.redisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
        return res;
    }

    public Set<?> pop(String key) {
        return (Set<?>) this.redisTemplate.opsForSet().pop(key);
    }

    public Set<?> pop(String key, Long length) {
        return (Set<?>) this.redisTemplate.opsForSet().pop(key, length);
    }

    public Long size(String key) {
        return this.redisTemplate.opsForSet().size(key);
    }

    public Long listSize(String key) {
        return this.redisTemplate.opsForList().size(key);
    }

    public Long unionAndStore(List<String> keys, String destKey, Long expireTime) {
        Long res = this.redisTemplate.opsForSet().unionAndStore(keys, destKey);
        this.redisTemplate.expire(destKey, expireTime, TimeUnit.MINUTES);
        return res;
    }

    public Long differenceAndStore(List<String> keys, String destKey, Long expireTime) {
        Long res = this.redisTemplate.opsForSet().differenceAndStore(keys, destKey);
        this.redisTemplate.expire(destKey, expireTime, TimeUnit.MINUTES);
        return res;
    }

    public Set<Object> setMembers(String key) {
        return this.redisTemplate.opsForSet().members(key);
    }

    public Boolean SISMember(String key) {
        return this.redisTemplate.opsForSet().isMember(key, (Object) null);
    }

    public Long remove(String key, Object... objects) {
        return this.redisTemplate.opsForSet().remove(Arrays.toString(objects));
    }

    public Boolean SISMember(String key, Object object) {
        return this.redisTemplate.opsForSet().isMember(key, object);
    }

    public void putIntegerMap(String key, Map<String, Integer> map, Long expireTime) {
        this.redisTemplate.opsForHash().putAll(key, map);
        this.redisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
    }

    public Map<Object, Object> getIntegerHashValue(String key) {
        return this.redisTemplate.opsForHash().entries(key);
    }

    public void putMapSet(String key, Map<String, Set<?>> map, Long expireTime) {
        this.redisTemplate.opsForHash().putAll(key, map);
        this.redisTemplate.expire(key, expireTime, TimeUnit.MINUTES);
    }

    public void putStringMap(String key, Map<String, String> map, Long expireTime, TimeUnit timeUnit) {
        this.redisTemplate.opsForHash().putAll(key, map);
        this.redisTemplate.expire(key, expireTime, timeUnit);
    }

    public void putStringMap(String key, Map<String, String> map) {
        this.redisTemplate.opsForHash().putAll(key, map);
    }

    public Map<Object, Object> getStringHashValue(String key) {
        return this.redisTemplate.opsForHash().entries(key);
    }

    public String listLeftPushCmdLogCalculateQueue(String key, Long expireTime) {
        Map<String, Object> lockMap = this.getLock(DISTRIBUTE_LOCK_KEY_FOR_CMD_LOG, 5000L);
        boolean getLockSuccess = (Boolean) lockMap.get("lock");
        String lockValue = (String) lockMap.get("lockValue");
        if (getLockSuccess) {
            return this.doLogic(key, expireTime, lockValue);
        } else {
            for (int i = 0; i < RETRY_TIMES; ++i) {
                lockMap = this.getLock(DISTRIBUTE_LOCK_KEY_FOR_CMD_LOG, 5000L);
                getLockSuccess = (Boolean) lockMap.get("lock");
                lockValue = (String) lockMap.get("lockValue");
                if (getLockSuccess) {
                    return this.doLogic(key, expireTime, lockValue);
                }
            }

            LogUtils.info(log, "重试{}次未获得分布式锁,本次业务执行失败", RETRY_TIMES);
            return null;
        }
    }

    private String doLogic(String key, Long expireTime, String lockValue) {
        LogUtils.info(log, "获得分布式锁: {}, 执行业务逻辑", DISTRIBUTE_LOCK_KEY_FOR_CMD_LOG);
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
        String value = LocalDateTime.now().format(dateTimeFormatter);
        this.listLeftPush(key, value, expireTime);
        this.releaseLock(DISTRIBUTE_LOCK_KEY_FOR_CMD_LOG, lockValue);
        return value;
    }

    public Map<String, Object> getLock(String lockId, long millisecond) {
        Map<String, Object> map = new HashMap<>();
        String lockValue = UUID.randomUUID().toString();
        Boolean success = this.stringRedisTemplate.opsForValue().setIfAbsent(lockId, lockValue, millisecond, TimeUnit.MILLISECONDS);
        map.put("lockValue", lockValue);
        map.put("lock", success != null && success);
        return map;
    }

    public void releaseLock(String lockId, String lockValue) {
        String value = this.stringRedisTemplate.opsForValue().get(lockId);
        if (lockValue.equals(value)) {
            this.stringRedisTemplate.delete(lockId);
            LogUtils.info(log, "分布式锁ID: {}, 已释放", lockValue);
        } else {
            LogUtils.info(log, "分布式锁ID: {}, 已过期", lockValue);
        }

    }

    public void zSetAdd(String key, String value, double score) {
        this.stringRedisTemplate.opsForZSet().add(key, value, score);
    }

    public void zSetRemove(String key, List<String> values) {
        values.forEach((item) -> this.stringRedisTemplate.opsForZSet().remove(key, item));
    }

    public void zSetRemove(String key, String value) {
        this.stringRedisTemplate.opsForZSet().remove(key, value);
    }

    public Long getHashKeyExpireTime(String key, TimeUnit timeUnit) {
        return this.stringRedisTemplate.opsForHash().getOperations().getExpire(key, timeUnit);
    }

    public void setAdd(String key, String value) {
        this.stringRedisTemplate.opsForSet().add(key, value);
    }

    public Long setAddAndTransToStr(String key, Object value) {
        return this.stringRedisTemplate.opsForSet().add(key, JSONObject.toJSONString(value));
    }

    public Long setRemoveAndTransToStr(String key, Object value) {
        return this.stringRedisTemplate.opsForSet().remove(key, JSONObject.toJSONString(value));
    }

    public Set<Object> setMembers(String key, Class<?> clazz) {
        Set<String> members = this.stringRedisTemplate.opsForSet().members(key);
        Set<Object> set = new HashSet<>();
        if (!CollectionUtils.isEmpty(members)) {
            members.forEach((member) -> set.add(JSONObject.parseObject(member, clazz)));
        }

        return set;
    }

    public Long getExpire(String key, TimeUnit timeUnit) {
        return this.redisTemplate.getExpire(key, timeUnit);
    }
}
